package com.vn.ld.order.service.management;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.activation.UnsupportedDataTypeException;
import javax.naming.TimeLimitExceededException;

import org.apache.commons.lang.time.StopWatch;
import org.apache.log4j.Logger;

import com.vn.ld.btc.dao.entity.Order;
import com.vn.ld.btc.dao.entity.Transaction;
import com.vn.ld.common.Constants;
import com.vn.ld.order.domain.ModificationType;
import com.vn.ld.order.domain.ModifiedDBObject;
import com.vn.ld.order.domain.ModifiedOrder;
import com.vn.ld.order.domain.ModifiedTransaction;
import com.vn.ld.order.domain.OrderList;
import com.vn.ld.order.domain.queue.manager.DbQueueManager;

public class OrderManager {
	
	Logger logger = Logger.getLogger(OrderManager.class);
	
	private static OrderManager orderManager = null;

	private List<Order> buyOrderList = new OrderList();
	private List<Order> sellOrderList = new OrderList();
	
	//Queue for inserting to DB
	private BlockingQueue<Queue<ModifiedDBObject>> dbQueue = new LinkedBlockingQueue<Queue<ModifiedDBObject>>();
	
	
	private MemDBSynchStatus memDbDiffStatus = MemDBSynchStatus.SYNCH;
	
	public enum MemDBSynchStatus{
		SYNCH,
		DESYNCH
	}
	
	private OrderManager(){
		Thread dbQueueManagerThread = new Thread(new DbQueueManager(dbQueue), "Db queue manager thread");
		dbQueueManagerThread.start();
	}
	
	public static OrderManager getInstance() {

		if (orderManager == null) {
			orderManager = new OrderManager();
			return orderManager;
		} else
			return orderManager;

	}
	
	/**
	 * Main method of order manager - it's enough to invoke this method and the OrderManager will take care of the order :)
	 * @param order
	 * @return
	 * @throws UnsupportedDataTypeException
	 * @throws TimeLimitExceededException throws when waiting for DBQueue to be emptied
	 */
	public boolean manageOrder(Order order) throws UnsupportedDataTypeException, TimeLimitExceededException{
		
		//check if netting possible,
		if(checkNettingPossible(order)){ 
			
			List<ModifiedDBObject> listOfModifiedDBObjects = new LinkedList<>();
			
			List<Order> listOfOrders = doMemoryNetting(order, listOfModifiedDBObjects);
			
			dbQueue.add(new LinkedList<>(listOfModifiedDBObjects));
			
			//check the size and eventually refresh the list
			refreshTheList(listOfOrders);
		}
		else{
			//if netting not possible add to collection
			List<Order> listOfOrders = addOrderToList(order);
			//send order to DBQueue
			Queue<ModifiedDBObject> listOfNewOrders = new LinkedList<>();
			listOfNewOrders.add(new ModifiedOrder(order, ModificationType.ADD));
			
			dbQueue.add(listOfNewOrders);
			
			//check the size and eventually clean the list
			shrinkTheList(listOfOrders);
		}	
		
		return true;
	}
	
	protected List<Order> refreshTheList(List<Order> listOfOrders) throws TimeLimitExceededException {
		
		if (memDbDiffStatus.equals(MemDBSynchStatus.DESYNCH) && listOfOrders.size() < Constants.MIN_ORDER_LIST_SIZE ){
			
			//Wait for the DB Queue to be emptied //!!! carrefour!!!
			StopWatch stopWatch = new StopWatch();
			stopWatch.start();
			
			while (!dbQueue.isEmpty()){
				if(stopWatch.getTime() > Constants.DBQUEUE_WAIT_EMPTY_TIMEOUT){
					String errorMsg = "Timeout when waiting for the DB Queue to be emptied";
					logger.error(errorMsg);
					throw new TimeLimitExceededException(errorMsg);
				}
				
			}
			
			//TODO take the orders from DB by UnitPrice, date limit by Constants.DEF_ORDER_LIST_SIZE
			
			//empty the list Of Orders
			listOfOrders.clear();
			//TODO insert into the list of Orders the orders taken from DB
			
			//set the Synch status
			if(listOfOrders.size() < Constants.DEF_ORDER_LIST_SIZE)
				memDbDiffStatus = MemDBSynchStatus.SYNCH;
		}
			
		return null;
		
	}

	/**
	 * Shrinks the list to DEF_ORDER_LIST_SIZE if the list is too big
	 * @param listOfOrders
	 */
	protected List<Order> shrinkTheList(List<Order> listOfOrders){
		if(listOfOrders.size() > Constants.MAX_ORDER_LIST_SIZE){
			listOfOrders.subList(Constants.DEF_ORDER_LIST_SIZE, listOfOrders.size()).clear();
			
			memDbDiffStatus = MemDBSynchStatus.DESYNCH;
		}		
		return listOfOrders;
	}
	
	protected List<Order> addOrderToList(Order order){
		
		List<Order> orderList = null;
		
		if(order.getAssetType().equals(Constants.BUY_SIDE)){
			buyOrderList.add(order);
			orderList = buyOrderList;
		}
		else if (order.getAssetType().equals(Constants.SELL_SIDE)){
			sellOrderList.add(order);
			 orderList = sellOrderList;
		}
		return orderList;
	}
	

	/**
	 * Method that should quickly check if the netting is possible
	 * @param order
	 * @param orderList - buyList or sellList
	 * @return
	 * @throws UnsupportedDataTypeException
	 */
	protected boolean checkNettingPossible(Order order) throws UnsupportedDataTypeException{
			
		if(order.getAssetType() == Constants.BUY_SIDE){		
			if(    sellOrderList.isEmpty()
				|| order.getUnitPrice().compareTo(sellOrderList.get(0).getUnitPrice()) == -1)
				return false;
			else 
				return true;
		
		}else if (order.getAssetType() == Constants.SELL_SIDE){
			if(order.getUnitPrice().compareTo(buyOrderList.get(0).getUnitPrice()) == 1)
				return false;
			else
				return true;
		}else{
			String errorMsg = "Unknown datatype" +  order.getAssetType();
			logger.fatal(errorMsg);
			throw new UnsupportedDataTypeException(errorMsg);
			
		}
		
	}
	/**
	 * 
	 * @param order
	 * @param listOfModifiedOrders
	 * @param listOfNewTransactions
	 * @return
	 */
	protected List<Order> doMemoryNetting(   Order order
										   , List<ModifiedDBObject> listOfModifiedDBObjects){
		
		//TODO
		//manage abnormal case when the list in memory is in desynch with DB and the order empties the list in mem
		//do netting in mem
		
		List<Order> contradictedOrderList = getContradictedOrderList(order);
		
		//add also order (not from orderList) to the list
		doNettingLogic(order, listOfModifiedDBObjects, contradictedOrderList);
		
		List<ModifiedOrder> listOfModifiedOrders = getListOfModifiedOrders(listOfModifiedDBObjects);
		
		//Update order list
		updateOrderList(listOfModifiedOrders);
		
		return contradictedOrderList;
		
	}

	/**
	 * @param listOfModifiedDBObjects
	 * @return
	 */
	private List<ModifiedOrder> getListOfModifiedOrders(List<ModifiedDBObject> listOfModifiedDBObjects) {
		List<ModifiedOrder> listOfModifiedOrders = new ArrayList<>();

		for (ModifiedDBObject modObject : listOfModifiedDBObjects) {			
			if(modObject instanceof ModifiedOrder)
				listOfModifiedOrders.add((ModifiedOrder)modObject);
		}
		return listOfModifiedOrders;
	}

	/**
	 * @param order
	 * @param listOfModifiedOrders
	 * @param listOfNewTransactions
	 * @param contradictedOrderList
	 */
	private void doNettingLogic(Order order, List<ModifiedDBObject> listOfModifiedDBObjects, List<Order> contradictedOrderList) {
		for (Order orderFromList : contradictedOrderList) { 
				
				if(    	(  order.getAssetType().equals(Constants.BUY_SIDE) 
						&& order.getUnitPrice().compareTo(orderFromList.getUnitPrice()) == -1)
					||
						(  order.getAssetType().equals(Constants.SELL_SIDE) 
						&& order.getUnitPrice().compareTo(orderFromList.getUnitPrice()) == 1)){ //buy<sell no match, 
					
					listOfModifiedDBObjects.add(new ModifiedOrder(order, ModificationType.ADD));//add an order to the list and break the loop
					break;																
				}
				else{ //match
					BigDecimal remainVol = order.getRemainVolume().subtract(orderFromList.getRemainVolume());
					order.setRemainVolume(remainVol);
					
					Transaction transaction = new Transaction();
					//TODO set transaction values
					
					listOfModifiedDBObjects.add(new ModifiedTransaction(transaction));
					
					if (remainVol.compareTo(new BigDecimal(0)) == 0){ //remainVol = 0
						listOfModifiedDBObjects.add(new ModifiedOrder(orderFromList, ModificationType.DELETE)); // add here info about order deletion and delete from orderList this order orderFromList
						order.setStatus(Constants.OrderStatus.ORDER_STATUS_COMPLETED);
						break; //stop right here
						
					}else if (   (  order.getAssetType().equals(Constants.BUY_SIDE) 
							  	 && remainVol.compareTo(new BigDecimal(0)) == 1)
							  || 
							     (  order.getAssetType().equals(Constants.SELL_SIDE) 
							  	 && remainVol.compareTo(new BigDecimal(0)) == -1)){ //remainVol > 0
						
					
						order.setRemainVolume(remainVol);
						listOfModifiedDBObjects.add(new ModifiedOrder(orderFromList, ModificationType.DELETE)); // add here info about order deletion and delete from orderList this order orderFromList
						//do the loop once again
					}else{ //remainVol < 0
						orderFromList.setRemainVolume(remainVol.abs());
						listOfModifiedDBObjects.add(new ModifiedOrder(orderFromList, ModificationType.UPDATE));
						order.setStatus(Constants.OrderStatus.ORDER_STATUS_COMPLETED);
						listOfModifiedDBObjects.add(new ModifiedOrder(order, ModificationType.ADD));
						break; //stop right here
					}
				}
		}
	}

	private void updateOrderList(List<ModifiedOrder> listOfModifiedOrders) {
		for (ModifiedOrder modifiedOrder : listOfModifiedOrders) {
			
			Order order = (Order)modifiedOrder.getObject();			
			
			List<Order> orderList = getProperOrderList(order);
			
			//Delete from the orderList
			if(modifiedOrder.getModificationType().equals(ModificationType.DELETE))
				orderList.remove(modifiedOrder.getObject());
			
			//add to the orderList
			if(modifiedOrder.getModificationType().equals(ModificationType.ADD))
				orderList.add((Order)modifiedOrder.getObject());
			
		}
	}

	private List<Order> getProperOrderList(Order order) {
		List<Order> orderList = null;		
		
		if(order.getAssetType().equals(Constants.BUY_SIDE)){
			orderList = buyOrderList;
		}
		else if (order.getAssetType().equals(Constants.SELL_SIDE)){
			 orderList = sellOrderList;
		}
		return orderList;
	}
	
	private List<Order> getContradictedOrderList(Order order) {
		List<Order> orderList = null;		
		
		if(order.getAssetType().equals(Constants.BUY_SIDE)){
			orderList = sellOrderList;
		}
		else if (order.getAssetType().equals(Constants.SELL_SIDE)){
			 orderList = buyOrderList;
		}
		return orderList;
	}

	public BlockingQueue<Queue<ModifiedDBObject>> getDbQueue() {
		return dbQueue;
	}
	

}
